Este un obiect de sincronizare care poate fi deținut (posedat, acaparat) doar de un singur proces/thread (în funcție de implementare) la un moment dat. Drept urmare, operațiile de baza cu mutex-uri sunt cele de obținere și de eliberare.
Odată obținut de un proces/thread, un mutex devine indisponibil pentru orice alt proces/thread. Orice proces/thread care încearcă să acapareze un mutex indisponibil, se va bloca (un timp definit sau nu) așteptând ca el să devină disponibil.
Mutex-urile sunt cel mai des folosite pentru a permite unui singur proces la un moment dat să acceseze o resursă.
Semafoarele sunt resurse IPC folosite pentru sincronizarea între procese/thread-uri (e.g., pentru controlul accesului la resurse). Un semafor poate fi privit ca un contor ce poate fi incrementat și decrementat, dar a cărui valoare nu poate scădea sub 0. Atât timp cât semaforul (contorul) are valori strict pozitive el este considerat disponibil. Când valoarea semaforului a ajuns la 0, el devine indisponibil și următoarea încercare de decrementare va duce la o blocare a threadului/procesului de pe care s-a făcut apelul până când semaforul devine disponibil.
Sunt folosite de procese pentru a comunica între ele prin mesaje. Aceste mesaje își păstrează ordinea în interiorul cozii de mesaje. Sunt mecanisme de comunicare unidirecționale atât pe Linux, cât și pe Windows.
Acest mecanism permite comunicarea între procese prin accesul direct și partajat la o zonă de memorie bine determinată. Este un mod mai rapid de comunicare între procese decât celelalte mijloace IPC, dar are un mare dezavantaj, procesele ce comunică trebuie să fie pe aceeași mașină (spre deosebire de sockeți, pipe-urile cu nume și cozile de mesaje din Windows)
Obiectele de tip IPC pe care se concentrează laboratorul de față sunt gestionate global de sistem și rămân în viață chiar dacă procesul creator moare. Faptul că aceste resurse sunt globale în sistem are implicații contradictorii:
Dacă un proces se termină, datele plasate în obiecte IPC pot fi accesate ulterior de alte procese
Pe de altă parte, procesul proprietar trebuie să se ocupe și de dealocarea resurselor, altfel ele rămân în sistem până la ștergerea lor manuală sau până la repornirea sistemului.
Faptul că obiectele IPC sunt globale în sistem poate duce la apariția unor probleme: cum numărul de mesaje care se afla în cozile de mesaje din sistem e limitat global, un proces care trimite multe asemenea mesaje poate bloca toate celelalte procese.
Atenție! Pentru folosirea API-ului trebuie să includeți la linking biblioteca rt (-lrt).
Semafoarele sunt resurse IPC folosite pentru sincronizarea între procese (e.g., pentru controlul accesului la resurse). Operațiile asupra unui semafor pot fi de setare, verificare a valorii ( care poate fi >= 0 ), test and set. Un semafor poate fi privit ca un contor ce poate fi incrementat și decrementat, dar a cărui valoare nu poate scădea sub 0.
Semafoarele POSIX sunt de 2 tipuri:
cu nume - folosite în general pentru sincronizare între procese distincte;
fără nume - ce pot fi folosite pentru sincronizarea între firele de execuție ale aceluiași proces, sau între procese - cu condiția ca semaforul să fie într-o zonă de memorie partajată.
În continuare vor fi luate în discuție semafoarele cu nume. Diferențele față de cele fără nume apar în funcțiile de creare și distrugere, celelalte funcții fiind identice.
ambele tipuri de semafoare sunt reprezentate în cod prin tipul sem_t.
semafoarele cu nume sunt identificate la nivel de sistem printr-un șir de forma ”/nume”.
fișierele antet necesare sunt <fcntl.h>, <sys/types.h> și <semaphore.h>.
Comportamentul este similar cu cel de la deschiderea fișierelor. Dacă flag-ul O_CREAT este prezent, trebuie folosită cea de-a doua formă a funcției, specificând permisiunile și valoarea inițială.
Singurele posibilități pentru al doilea argument sunt:
0 - se deschide semaforul dacă exista
O_CREAT - se creează semaforul dacă nu exista; se deschide dacă exista
O_CREAT | O_EXCL - se creează semaforul numai dacă nu exista; se întoarce eroare dacă exista
Un proces închide (notifică faptul că nu mai folosește) un semafor printr-un apel sem_close:
int sem_close(sem_t *sem);
Un proces poate șterge un semafor printr-un apel sem_unlink:
int sem_unlink(constchar*name);
Distrugerea efectivă a semaforului are loc după ce toate procesele care îl au deschis apelează sem_close sau se termină. Totuși, chiar și în acest caz, apelul sem_unlink nu se va bloca!
În funcție de flag-uri (unul dintre cele de mai jos trebuie specificat), coada poate fi deschisă pentru:
recepționare (O_RDONLY)
trimitere (O_WRONLY)
recepționare și trimitere (O_RDWR)
Daca attr e NULL, coada va fi creată cu atribute implicite. Structura mq_attr arată astfel:
struct mq_attr {long mq_flags;/* 0 or O_NONBLOCK */long mq_maxmsg;/* Max. number of messages on queue */long mq_msgsize;/* Max. message size (bytes) */long mq_curmsgs;/* number of messages currently in queue */};
Pentru a trimite un mesaj (de lungime cunoscută, stocat într-un buffer) în coadă se apelează mq_send:
int mq_send(mqd_t mqdes,constchar*buffer, size_t length,unsigned priority);
Mesajele sunt ținute în coadă în ordinea descrescătoare a priorității.
În cazul în care coada este plină, apelul blochează. Dacă este o coadă non-blocantă (O_NONBLOCK), funcția va întoarce -1, iar errno va fi setat la EAGAIN.
Pentru a primi un mesaj dintr-o coadă (și anume: cel mai vechi mesaj cu cea mai mare prioritate) se folosește mq_receive:
Dacă priority este non-NULL, zona de memorie către care face referire va reține prioritatea mesajului extras.
În cazul în care coada este vidă, apelul blochează. Dacă este o coadă non-blocantă (O_NONBLOCK), comportamentul este similar cu cel al mq_send.
Atenție! La primirea unui mesaj, lungimea buffer-ului trebuie să fie cel puțin egală cu dimensiunea maximă a mesajelor pentru coada respectivă, iar la trimitere cel mult egală. Dimensiunea maximă implicită se poate afla pe Linux din /proc/sys/kernel/msgmax.
Pentru a putea utiliza o zonă de memorie partajată după deschidere, aceasta trebuie mapată în spațiul de memorie al procesului. Maparea se realizează printr-un apel mmap:
Valoarea întoarsă reprezintă un pointer către începutul zonei de memorie sau MAP_FAILED în caz de eșec. Acest apel are o largă aplicabilitate și va fi discutat în cadrul laboratorului de memorie virtuală. Momentan, pentru a mapa întregul conținut al unei zone (shm_fd) de dimensiune cunoscută (shm_len), recomandăm folosirea apelului:
mem = mmap(0, shm_len, PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd,0);
Când maparea nu mai este necesară, prin apelul munmap se realizează demaparea:
Închiderea unei zone de memorie partajată este identică cu închiderea unui fișier - apelul close.
Odată ce o zonă de memorie a fost demapată și închisă în toate procesele implicate, se poate șterge prin shm_unlink:
int shm_unlink(constchar*name);
Semantica este identică cu cea de la funcțiile *_unlink anterioare - ștergerea efectivă este amânată până ce toate procesele implicate închid zona în cauză sau se termină.
Conținutul cozilor (conținutul mesajelor) nu poate fi vizualizat, însă informații statistice pot fi obținute prin montarea unui pseudo-sistem de fișiere:
Sistemul de operare Windows pune la dispoziție o serie de mecanisme de comunicare și schimb de date între aplicații. Cazul de care ne vom ocupa este doar cel în care aceste aplicații sunt procese care rulează pe aceeași mașină.
Înainte de a fi prezentate mecanismele de comunicare în sine trebuie introduse mecanismele de sincronizare, care sunt folosite pentru controlul accesului la resurse.
Mecanismele de sincronizare oferite de sistemul de operare Windows sunt mai multe și mai complexe decât cele din Linux. Pentru sincronizare sunt necesare:
WAIT_ABANDONED - Obiectul specificat este un mutex care a fost abandonat, adică thread-ul care-l deținea s-a terminat fără să-l elibereze. În acest caz threadul curent va deveni deținătorul mutexului iar starea mutexului va fi nonsignaled (mutex ocupat).
WAIT_IO_COMPLETION - Așteptarea a fost întreruptă de un apel asincron de procedură.
WAIT_TIMEOUT - Timpul de expirare s-a scurs.
WAIT_FAILED - Funcția a eșuat. Informații despre eroare pot fi obținute folosind funcția GetLastError().
În continuare sunt prezentate pe larg alte funcții care fac parte din această categorie :
SignalObjectAndWait semnalizează un obiect și așteaptă după altul. Funcția are sintaxa :
Aceste funcții așteaptă după mai multe obiecte de sincronizare. Execuția lor se termină când una din următoarele condiții este adevărată:
Starea unui obiect de sincronizare SAU starea tuturor obiectelor de sincronizare este 'signaled' (depinde de parametri)
Timpul de așteptare (time-out) a expirat. Acest timp poate fi setat ca INFINITE pentru a specifica faptul că timpul de așteptare nu va expiră niciodată
Sunt operații prin care se obține un HANDLE al unui obiect de tip mutex. Este necesar doar un singur apel, fie el de creare sau de deschidere (se presupune ca alt proces a creat deja mutex-ul).
Pentru a crea un mutex se folosește funcția CreateMutex cu sintaxa :
Obținerea unui mutex se realizează folosind una din funcțiile de așteptare tratate anterior.
Încercarea de acaparare a unui mutex presupune următorii pași:
se verifică daca mutex-ul este disponibil
daca da, îl pot acapara și devine indisponibil, și funcția întoarce succes
daca nu, aștept să devină disponibil, după care îl acaparez, și funcția întoarce succes
la time-out funcția întoarce eroare (atenție: e posibil să nu existe time-out)
Încercarea de obținere se poate face cu sau fară timp de expirare (time-out) în funcție de parametrii dați funcțiilor de așteptare. Cea mai des folosită funcție de așteptare este WaitForSingleObject.
Operația de distrugere a unui mutex este aceeași ca pentru orice HANDLE. Se folosește funcția CloseHandle. După ce toate HANDLE-urile unui mutex au fost închise, mutexul este distrus și resursele ocupate de acesta eliberate.
Atenție! La terminarea execuției unui program toate HANDLE-urile folosite de acesta sunt automat închise. Deci, spre deosebire de semafoarele IPC din Linux, este imposibil ca un mutex (sau semafor) în Windows să mai existe în sistem după ce programele care l-au folosit/creat s-au terminat.
Un semafor este un obiect de sincronizare care are intern un contor ce ia doar valori pozitive. Atât timp cât semaforul (contorul) are valori strict pozitive el este considerat disponibil (signaled). Când valoarea semaforului a ajuns la zero el devine indisponibil (nonsignaled) și următoarea încercare de decrementare va duce la o blocare a threadului/procesului de pe care s-a făcut apelul până când semaforul devine disponibil.
Operația de decrementare se realizează doar cu o singură unitate (la fel ca în API-ul POSIX), în timp ce incrementarea se poate realiza cu orice valoare în limita maximă.
Funcția de creare a semafoarelor este CreateSemaphore și are sintaxa :
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpNAME
);
Această funcție se poate folosi și pentru deschiderea unui semafor deja existent. Alternativ, pentru a folosi un semafor deja existent, este necesar obținerea HANDLE-ului semaforului, operație ce se realizează folosind funcția OpenSemaphore cu următoarea sintaxă :
Operația de decrementare a semaforului cu sau fără așteptare se realizează folosind una din funcțiile de așteptare. Cea mai des folosită este funcția WaitForSingleObject.
Operația de distrugere a unui semafor este similară cu cea de distrugere a unui mutex. Se folosește funcția CloseHandle. După ce toate HANDLE-urile unui semafor au fost închise, semaforul este distrus și resursele ocupate de acesta eliberate.
Cozile de mesaje sunt un fel de pseudo-fișiere care rezidă în memorie. De aceea, pot fi gestionate prin intermediul funcțiilor standard de acces la fișiere. Fiind păstrate în memorie, toate aceste date au un caracter volatil, spre deosebire de fișiere, iar când toate handle-urile la un mailslot sunt distruse, acesta la rândul său, este distrus împreună cu datele, iar memoria este eliberată (spre deosebire de cozile de mesaje de pe Linux).
Au următoarele caracteristici:
Sunt unidrecționale.
Pot exista mai mulți cititori și mai mulți scriitori, dar cel mai frecvent se folosește o arhitectură one-to-many.
Un scriitor nu știe sigur dacă mesajul său a ajuns la cititor.
Dimensiunea mesajelor e limitată.
Datorită modului de numire, se pot transmite mesaje prin rețea.
Detalii despre limitări
Mesajele de tip broadcast sunt limitate la maximum 424 bytes, iar încercarea de a trimite un mesaj broadcast mai mare va eșua, iar funcția va întoarce eroare.
NU pot fi trimise mesaje de lungime 425 bytes sau 426 bytes.
Lungimea maximă a unui mesaj este 64 Kbytes.
Un exemplu tipic de folosire este următorul:
serverul mailslot creează coada folosind CreateMailslot, apoi așteaptă să primească un mesaj folosind un apel ReadFile
clientul mailslot deschide coada folosind CreateFile, apoi transmite un mesaj folosind un apel WriteFile.
Când un proces creează un mailslot, trebuie să-i atribuie o denumire de forma:
\\.\mailslot\[path]<nume>
Atenție! Prefixul “\\.\mailslot\” trebuie să existe exact în această formă, el fiind urmat de un nume, care eventual va fi precedat de o cale. Calea este asemănătoare cu cea a fișierelor. Un exemplu valid: “\\.\mailslot\test\commands”.
Cozile de mesaje pot fi folosite și pentru a comunica cu procese care rulează pe alte calculatoare. În acest caz, clientul va folosi denumiri care au structura:
\\<ComputerName>\mailslot\[path]<Nume>
Pentru a trimite mesaje unui întreg domeniu, denumirea va avea structura:
\\<DomainName>\mailslot\[path]<Nume>
Pentru a trimite mesaje tuturor, denumirea va avea structura:
\\*\mailslot\[path]<Nume>
Pentru a crea o coadă de mesaje, se folosește funcția CreateMailslot care are următoarea sintaxă și întoarce un handle:
Pentru a deschide o coada de mesaje pentru scriere, se folosește funcția CreateFile care va primi în loc de numele fișierului denumirea cozii de mesaje care se dorește a fi deschisă și flagul FILE_SHARE_READ. Pentru a permite accesul concomitent al mai multor clienți, trebuie adăugat și flagul FILE_SHARE_WRITE.
Singura caracteristică a unei cozi de mesaje, care poate fi schimbată după ce coada a fost creată, este timpul de expirare. (Dimensiunea maximă a mesajelor acceptate de o coadă nu mai poate fi schimbată după ce aceasta a fost creată)
Funcția care setează această caracteristică este SetMailslotInfo și are următoarea sintaxă:
Memoria partajată permite accesul mai multor procese la un fișier ca și când fișierul ar fi o zonă de memorie. Astfel se pot folosi toate operațiile aplicabile asupra memoriei, inclusiv pointeri.
O facilitate specială a FileMapping este aceea de memorie partajată identificată după nume (named shared memory).
Atenție! Accesul la o zonă de memorie partajată trebuie reglementat folosind unul din mecanismele de sincronizare descrise mai sus!
Pentru a demapa o zonă de memorie partajată, care a fost anterior mapată folosind funcția MapViewOfFile(), se folosește funcția UnmapViewOfFile care are următoarea sintaxă :
Serverul creează o zonă de memorie partajată, iar apoi așteaptă un interval de timp. Clientul deschide zona de memorie partajată și scrie un mesaj la începutul ei. Serverul termină așteptarea și afișează conținutul zonei de memorie.
Asigurați-vă că în timpul rulării exercițiilor nu există resurse IPC cu același nume create anterior. Folosiți comanda make clean care va șterge resursele alocate (și eventual prost eliberate)
Nu confundați variabila globală errno cu valoarea de retur a unei funcții. Un apel de sistem va întoarce o anumită valoare de return în caz de eroare și va seta variabila globală errno astfel încât să indice motivul erorii.
Întrucât la toate exercițiile veți avea nevoie de două console deschise în paralel, pe Linux, este recomandat să vă instalați terminator.
sudoapt-getinstall terminator
Ctrl+Shift+O -> open hOrizontal tab
Ctrl+Shift+E -> open vErtical tab
Ctrl+Shift+N -> move to Next tab
Ctrl+Shift+P -> move to Previous tab
Ctrl+Shift+W -> close current tab
(2 puncte) Intrați în directorul 1-fun/:
Programul sem.c crează un semafor și îl incrementează de fiecare dată când apăsați o tastă.
Rulați programul și observați cum se schimbă conținutul fișierului /dev/shm/sem.my_sem la apăsarea unei taste. Ce se întâmplă cu fișierul /dev/shm/sem.my_sem când terminați normal programul? Dar când îl terminați folosind Ctrl+C?
console1$ ./sem
Press any key to continue(E/e to exit)
Programul shm.c crează o zonă de memorie partajata și scrie în ea un șir de caractere. Analizați conținutul fișierului /dev/shm/my_shm. Observați ce se întâmplă cu zona de memorie partajată când programul shm.c se încheie normal (apăsați orice tastă) sau când este întrerupt folosind Ctrl+C.
console1$ ./shm
Press any key to continue...
console2$ cat/dev/shm/my_shm |hexdump-c
Programul mq.c crează o coadă de mesaje și pune un mesaj în coadă la fiecare apăsare a unei taste. În Linux cozile de mesaje sunt create într-un sistem de fișiere virtual. Acest sistem de fisiere poate fi montat în ierarhia voastră de fișiere astfel:
Să se implementeze un protocol simplu client - server folosind mecanisme IPC. Serverul întreține o tabelă de dispersie (hashtable), conținând cuvinte, în care se fac inserări și ștergeri comandate de mesajele primite de la clienți. Inserarea într-un tablou (bucket) se face la finalul acestuia.
Clienții primesc operațiile prin argumentele primite în linia de comanda la lansarea în execuție. Exemplu:
./client a vincent c a test p
În acest caz, clientul va trimite serverului, în ordine, mesajele: a vincent pentru adăugarea în hashtable a cuvântul “vincent”, c pentru golirea tabelei, a test pentru adăugarea cuvântului “test” și p pentru afișarea conținutului tabelei.
Exercițiul se compune din 3 părți:
comunicarea prin mesaje - clienții trimit comenzi serverului prin intermediul unei cozi de mesaje
tabela de dispersie - serverul va menține tabela în memoria partajată, iar clientul va citi din această zonă de memorie de fiecare dată când are nevoie să printeze
sincronizarea accesului la tabelă - se va realiza prin semafoare
(0.5 puncte) Acomodarea cu codul deja existent
Urmăriți sursele din proiect:
sever.c - conține codul rulat de server
client.c - conține codul rulat de client
common.h - conține structurile necesare protocolului
generic_queue.h - header cu funcțiile generale pentru lucrul cu coada de mesaje
generic_shm.h - header cu funcțiile generale pentru lucrul cu memoria partajată
generic_sem.h - header cu funcțiile generale pentru lucrul cu semafoare
unix_*.c - conține implementarea unix a funcțiilor din generic_*.h
win_*.c - conține implementarea windows a funcțiilor din generic_*.h
hashtable.h, hashtable.c - reprezintă interfața și implementarea funcțiilor de lucru cu tabela de dispersie
hash.h, hash.c - reprezintă interfața și implementarea funcției de hash
Compilați și rulați serverul și clientul în două console diferite
Atenție! Fișierul common.h conține structurile necesare la următoarele exerciții
(2 puncte) Comunicare prin mesaje
Creați un server și un client care să comunice prin următoarele comenzi:
'a S': trimite serverului mesajul de adăugare în hashtable a cuvântului S (add);
'c': trimite serverului mesajul de golire a conținutului tabelei (clear);
'p': clientul afișează la standard output conținutul tabelei - formatul este precizat mai jos (print);
'e': clientul îi spune serverului să își încheie execuția (exit).
Trebuie să completați funcțiile msgq_* din fișierul unix_queue.c/win_queue.c relativ la interfața din fișierele common.h și generic_queue.h
Funcțiile de msgq_send și msgq_receive trebuie să trimită/primească toată structura message_t primită ca parametru (nu doar unul din câmpuri).
Aceste funcții sunt deja apelate din codul de server - server.c, respectiv client - client.c
Hint:
Urmăriți în sursa unix_queue.c/win_queue.c comentariile TODO 1
În această fază trebuie să funcționeze doar trimiterea mesajelor